package br.uvv.sao.dao;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.swing.SortOrder;

import org.hibernate.Criteria;
import org.hibernate.FetchMode;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;

import br.uvv.sao.util.DAOException;
import br.uvv.sao.util.HibernateUtil;

/**
 * DAO Generico
 * Todo DAO deve extender esta classe, para herdar os metodos basicos de acesso a banco
 * @author hermann
 *
 */
@SuppressWarnings({ "rawtypes" })
public class DAO implements Serializable {
	
	private static final long serialVersionUID = -831702468813953020L;
	
	public Session sessao;
	public Transaction transacao;
	
	/**
	 *  Metodo que pega a sessao corrente do hibernate e inicia uma transacao
	 */
	public void begin(){
		this.sessao = HibernateUtil.getSessionfactory().getCurrentSession();
		this.transacao = this.sessao.beginTransaction();
	}
	
	/**
	 * Metodo que faz o commit de uma transacao
	 */
	public void commit(){
		this.transacao.commit();
	}
	
	/**
	 * Metodo generico para pesquisa de retorno unico.
	 * @param classe Classe onde sera efetuada a pesquisa.
	 * @param fetchMode Nome dos atributos que devem ser carregados do banco junto com o objeto principal. 
	 * @param filtros Restricoes de busca na base de dados
	 * @return Um objeto generico, que deve ser convertido para a classe a qual foi feita a pesquisa
	 * @throws DAOException
	 */
	public Object get(Class classe, String[] fetchMode, Criterion... filtros) throws DAOException {
		Object model = null;
		try {
			begin();
			Criteria criteria = this.sessao.createCriteria(classe);
			if (filtros != null)
				for (Criterion c : filtros)
					if (c != null)
					criteria.add(c);
			if (fetchMode != null)
				for (String s : fetchMode)
						if (s != null)
					criteria.setFetchMode(s, FetchMode.JOIN);
			model = criteria.uniqueResult();
			commit();
		} catch (Exception e) {
			System.out.println("ERRO/DAO - Get -> " + classe);
			throw new DAOException(e);
		}
		return model;
	}
	
	/**
	 * Metodo generico para pesquisa por ID
	 * @param classe Classe onde a pesquisa sera executada
	 * @param id O id do objeto que se deseja recuperar
	 * @param fetchMode Os atributos que devem ser trazidos do banco junto com o objeto principal
	 * @return Um objeto generico, que deve ser convertido para a classe a qual foi feita a pesquisa
	 * @throws DAOException 
	 */
	public Object get(Class classe, Long id, String... fetchMode) throws DAOException {
		Object modelInstance = new Object();
		try {
			begin();
			Criteria criteria = this.sessao.createCriteria(classe);
			if (fetchMode != null)
				for(String s : fetchMode)
					if (s != null)
					criteria.setFetchMode(s, FetchMode.JOIN);
			criteria.add(Restrictions.eq("id", id));
			modelInstance = (Object) criteria.uniqueResult();
			commit();
		} catch(Exception e){
			System.out.println("ERRO/DAO - Get ID -> " + classe);
			throw new DAOException(e);
		}
		return modelInstance;
	}
	
	/**
	 * Metodo de persistencia generico. Ele verifica se o ID passado e null, significando que o objeto a ser persistido
	 * ainda nao foi salvo nenhuma vez. O Hibernate possui uma funcao chamada saveOrUpdate(), porem este metodo possui
	 * bugs conhecidos, assim usamos o .save() e o .update().
	 * @param modelInstance Objeto a ser persistido
	 * @param id O id do objeto que se deseja recuperar
	 * @return Verdadeiro caso a transacao ocorra com sucesso
	 * @throws DAOException
	 */
	public Boolean save(Object modelInstance, Long id) throws DAOException {
		try {
			begin();
			if (id == null){
				this.sessao.save(modelInstance);
			} else {
				this.sessao.update(modelInstance);
			}
			commit();
			return true;
		} catch (Throwable e) {
			System.out.println("ERRO/DAO - Save -> " + modelInstance.getClass().toString());
			throw new DAOException(e);
		}
	}
	
	/**
	 * Metodo de exclusao generico. Por garantia, ele busca o objeto no banco novamente e ai sim o remove.
	 * @param classe Classe onde a pesquisa sera executada
	 * @param id O id do objeto que se deseja recuperar
	 * @return Verdadeiro caso a transacao ocorra com sucesso
	 * @throws DAOException
	 */
	public Boolean delete(Class classe, Long id) throws DAOException {
		try{
			begin();
			this.sessao.delete(
				this.sessao.createCriteria(classe)
					.add(Restrictions.eq("id", id))
					.uniqueResult()
			);
			commit();
			return true;
		} catch(Exception e){
			System.out.println("ERRO/DAO - Delete -> " + classe.toString());
			throw new DAOException(e);
		}
	}
	
	/**
	 * Metodo generico para buscas paginadas de registros.
	 * @param first Define qual sera o primeiro registro a ser recuperado da base de dados
	 * @param pageSize Define a quantidade de registros que serao recuperados
	 * @param sortField Define o nome do campo de ordenacao
	 * @param sortOrder Um booleano, que define a order entre Ascendente e Descendente
	 * @param classe Classe onde a pesquisa sera executada
	 * @param fetchMode Os atributos que devem ser trazidos do banco junto com o objeto principal 
	 * @param filtros Conjunto de Restricoes (Restrictions), que serao utilizadas como filtro
	 * @return Lista de registros pesquisados, que batem com os filtros passados
	 * @throws DAOException
	 */
	public List listPaginator(int first, int pageSize, String sortField, Boolean sortOrder, 
			Class classe, String[] fetchMode, Criterion... filtros) throws DAOException {
		List modelList = new ArrayList();
		try {
			begin();
			Criteria criteria = sessao.createCriteria(classe);
			if (filtros != null)
				for (Criterion c : filtros)
					if(c != null)
						criteria.add(c);
			if (fetchMode != null)
				for (String s : fetchMode){
					criteria.setFetchMode(s, FetchMode.JOIN);
					String[] temp = s.split("[/.]");
					Integer count = temp.length;
					if (count > 1){
						if (count == 2)
							criteria.createCriteria(temp[0]);
						else if (count == 3)
							criteria.createCriteria(temp[0]).createCriteria(temp[1]);
						else if (count == 4)
							criteria.createCriteria(temp[0]).createCriteria(temp[1]).createCriteria(temp[2]);
					}
				}
			criteria.setFirstResult(first);
			criteria.setMaxResults(pageSize);
			String[] sortFields;
			if (sortField != null) {
				sortFields = sortField.split("[/.]");
				if (sortFields.length > 1){
					//Adicionar Sort Order Booleano
					if (sortOrder.equals(SortOrder.DESCENDING)){
						criteria.createCriteria(sortFields[0]).addOrder(Order.desc(sortFields[1]));
					} else if (sortOrder.equals(SortOrder.ASCENDING)){
						criteria.createCriteria(sortFields[0]).addOrder(Order.asc(sortFields[1]));
					}
				} else {
					if (sortOrder.equals(SortOrder.DESCENDING)){
						criteria.addOrder(Order.desc(sortField));
					} else if (sortOrder.equals(SortOrder.ASCENDING)){
						criteria.addOrder(Order.asc(sortField));
					}
				}
			}
			modelList = criteria.list();
			commit();
		} catch (Exception e) {
			System.out.println("ERRO/DAO - ListPaginator -> " + classe.toString());
			throw new DAOException(e);
		}
		return modelList;
	}
	
	/**
	 * Metodo de busca que não precisa ser paginado e nem ordenado
	 * @param classe Classe onde a pesquisa sera executada
	 * @param fetchMode Os atributos que devem ser trazidos do banco junto com o objeto principal 
	 * @param filtros Conjunto de Restricoes (Restrictions), que serao utilizadas como filtro
	 * @return Lista de registros pesquisados, que batem com os filtros passados
	 * @throws DAOException
	 */
	public List list(Class classe, String[] fetchMode, Criterion... filtros) throws DAOException {
		List modelList = new ArrayList();
		try {
			begin();
			Criteria criteria = sessao.createCriteria(classe);
			if (filtros != null)
				for (Criterion c : filtros)
					criteria.add(c);
			if (fetchMode != null)
				for (String s : fetchMode)
					criteria.setFetchMode(s, FetchMode.JOIN);
			modelList = criteria.list();
			commit();
		} catch (Exception e) {
			System.out.println("ERRO/DAO - List -> " + classe.toString());
			throw new DAOException(e);
		}
		return modelList;
	}
	
	/**
	 * Metodo que busca registros de uma classe, trazendo-os de forma ordenada
	 * @param classe Classe onde a pesquisa sera executada
	 * @param fetchMode Os atributos que devem ser trazidos do banco junto com o objeto principal 
	 * @param order Define se a ordenacao sera ascendente ou descendente
	 * @param filtros Conjunto de Restricoes (Restrictions), que serao utilizadas como filtro
	 * @return Lista de registros pesquisados, que batem com os filtros passados
	 * @throws DAOException
	 */
	public List list(Class classe, String[] fetchMode, Order order, Criterion... filtros) throws DAOException {
		List modelList = new ArrayList();
		try {
			begin();
			Criteria criteria = sessao.createCriteria(classe);
			if (filtros != null)
				for (Criterion c : filtros)
					if (c != null)
						criteria.add(c);
			if (fetchMode != null)
				for (String s : fetchMode)
					criteria.setFetchMode(s, FetchMode.JOIN);
			if (order!=null)
				criteria.addOrder(order);
			modelList = criteria.list();
			commit();
		} catch (Exception e) {
			System.out.println("ERRO/DAO - ListOrder -> " + classe.toString());
			throw new DAOException(e);
		}
		return modelList;
	}
	
	/**
	 * Metodo que retorna a contagem de registros de uma classe 
	 * @param classe Classe na qual sera feita a contagem de objetos
	 * @param filtros Conjunto de Restricoes (Restrictions), que serao utilizadas como filtro
	 * @return Um inteiro, no caso, o numero de registros que batem com os filtros passados
	 * @throws DAOException
	 */
	public Integer rowCount(Class classe, Criterion... filtros) throws DAOException {
		Long count = 0l;
		try {
			begin();
			Criteria criteria = sessao.createCriteria(classe);
			criteria.setProjection(Projections.rowCount());
			if (filtros != null){
				for (Criterion c : filtros)
					if(c != null)
						criteria.add(c);
			}
			count = (Long)criteria.uniqueResult();
			commit();
		} catch (Exception e) {
			System.out.println("ERRO/DAO - RowCount -> " + classe.toString());
			e.printStackTrace();
		}
		return Integer.parseInt(count.toString());
	}
}
